package com.dmj.sqldsl.wrapper;

import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toList;

import com.dmj.sqldsl.builder.DslQueryBuilder;
import com.dmj.sqldsl.builder.SelectBuilder;
import com.dmj.sqldsl.builder.column.ColumnBuilder;
import com.dmj.sqldsl.builder.column.NormalColumnsBuilder;
import com.dmj.sqldsl.builder.column.type.BooleanLambda;
import com.dmj.sqldsl.builder.column.type.ColumnLambda;
import com.dmj.sqldsl.builder.column.type.LongLambda;
import com.dmj.sqldsl.builder.column.type.NumberLambda;
import com.dmj.sqldsl.builder.column.type.StringLambda;
import com.dmj.sqldsl.builder.condition.ConditionBuilders;
import com.dmj.sqldsl.builder.condition.ConditionsBuilder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

public class QueryWrapper<T> extends Wrapper<T> {

  private SelectBuilder selectBuilder;
  private final List<OrderByCondition<T, ?>> orderByConditions;
  private List<ColumnLambda<T, ?>> groupByLambdas;
  private Limit limit;
  private ConditionsBuilder havingConditionsBuilder;

  @Data
  @AllArgsConstructor
  @NoArgsConstructor
  @EqualsAndHashCode
  static class OrderByCondition<T, R> {

    private ColumnLambda<T, R> lambda;
    private boolean isAsc;
  }

  @Data
  @AllArgsConstructor
  @NoArgsConstructor
  static class Limit {

    private int offset;
    private int size;
  }

  public QueryWrapper(Class<T> targetClass) {
    this(new SelectBuilder(), targetClass);
  }

  protected QueryWrapper(SelectBuilder selectBuilder, Class<T> targetClass) {
    super(targetClass);
    this.selectBuilder = selectBuilder;
    this.orderByConditions = new ArrayList<>();
    this.groupByLambdas = emptyList();
    this.havingConditionsBuilder = ConditionBuilders.empty();
  }

  public QueryWrapper<T> selectAs(ColumnBuilder<T, String> columnBuilder, StringLambda<T> alias) {
    this.selectBuilder.selectAs(columnBuilder, alias);
    return this;
  }

  public QueryWrapper<T> selectAs(ColumnBuilder<T, Long> columnBuilder, LongLambda<T> alias) {
    this.selectBuilder.selectAs(columnBuilder, alias);
    return this;
  }

  public QueryWrapper<T> selectAs(ColumnBuilder<T, Number> columnBuilder, NumberLambda<T> alias) {
    this.selectBuilder.selectAs(columnBuilder, alias);
    return this;
  }

  public QueryWrapper<T> selectAs(ColumnBuilder<T, Boolean> columnBuilder, BooleanLambda<T> alias) {
    this.selectBuilder.selectAs(columnBuilder, alias);
    return this;
  }


  @Override
  public final <R> QueryWrapper<T> orderBy(ColumnLambda<T, R> lambda, boolean isAsc) {
    this.orderByConditions.add(new OrderByCondition<>(lambda, isAsc));
    return this;
  }

  @Override
  @SafeVarargs
  public final <R> QueryWrapper<T> groupBy(ColumnLambda<T, R>... lambdas) {
    this.groupByLambdas = Arrays.asList(lambdas);
    return this;
  }

  @Override
  public QueryWrapper<T> having(Consumer<Wrapper<T>> wrapperConsumer) {
    QueryWrapper<T> wrapper = new QueryWrapper<>(getEntityClass());
    wrapperConsumer.accept(wrapper);
    this.havingConditionsBuilder = wrapper.getConditionsBuilder();
    return this;
  }

  @Override
  public QueryWrapper<T> limit(int size) {
    this.limit = new Limit(0, size);
    return this;
  }

  @Override
  protected DslQueryBuilder toQueryBuilder() {
    if (!groupByLambdas.isEmpty()) {
      List<ColumnBuilder<?, ?>> columnBuilders = groupByLambdas.stream()
          .map(ColumnLambda::getColumnBuilder).collect(toList());
      this.selectBuilder.add(new SelectBuilder(new NormalColumnsBuilder(columnBuilders)));
    }
    if (this.selectBuilder.isEmpty()) {
      this.selectBuilder = new SelectBuilder().selectAll(getEntityClass());
    }
    DslQueryBuilder from = this.selectBuilder
        .from(getEntityClass());
    DslQueryBuilder queryBuilder = from
        .where(super.getConditionsBuilder())
        .groupBy(groupByLambdas)
        .having(havingConditionsBuilder);
    orderByConditions.forEach(c -> queryBuilder.orderBy(c.getLambda(), c.isAsc()));
    if (this.limit != null) {
      return queryBuilder.limit(this.limit.getOffset(), this.limit.getSize());
    }
    return queryBuilder;
  }

  @Override
  protected QueryWrapper<T> createNewWrapper() {
    return new QueryWrapper<>(getEntityClass());
  }
}
